[Previous][Up][Next] |
Resource files and TResources class
One of the most important classes is TResources class, contained in resource unit, which represents a format-independent view of a resource file. In fact, while single resources are important, they are of little use alone, since they can't be read or written to file directly: they need to be contained in a TResources object.
TResources provides methods to read itself from a file or stream, using specific objects that are able to read resource data from such a stream: these are the so called resource readers, that descend from TAbstractResourceReader.
There are also resource writers that do the opposite, and that descend from TAbstractResourceWriter.
Usually readers and writers register themselves with TResources in the initialization section of the unit they are implemented in, so you only need to add a certain unit to your program uses clause to let TResources "know" about a particular file format.
Let's see a very simple example: a program that converts a .res file to an object file in COFF format (the object file format used by Microsoft Windows).
program res1; {$mode objfpc} uses Classes, SysUtils, resource, resreader, coffwriter; var resources : TResources; begin resources:=TResources.Create; resources.LoadFromFile('myresource.res'); resources.WriteToFile('myobject.o'); resources.Free; end.
As you can see, the code is trivial. Note that resreader and coffwriter units were added to the uses clause of the program: this way, the resource reader for .res files and the resource writer for COFF files have been registered, letting the resources object know how to handle these file types.
There are cases where one doesn't want to let the TResources object to choose readers and writers by itself. In fact, while generally it is a good idea to let TResources probe all readers it knows to find one able to read the input file, this isn't true when it comes to write files: writers are selected based on the file extension, so if you are trying to write a file with .o extension you can't be sure about which writer will be selected: it could be the COFF or the ELF writer (it depends on which writer gets registered first). Moreover, writers generally make an object file for the host architecture, so if you are running the program on a i386 machine it will produce a COFF or ELF file for i386.
The solution is to provide TResources with a specific writer. In the following example the reader is automatically chosen among various readers, and we use a specific writer to produce an ELF file for SPARC.
program res2; {$mode objfpc} uses Classes, SysUtils, resource, resreader, coffreader, elfreader, winpeimagereader, //readers elfwriter, elfconsts; var resources : TResources; writer : TElfResourceWriter; begin resources:=TResources.Create; resources.LoadFromFile(paramstr(1)); writer:=TElfResourceWriter.Create; writer.MachineType:=emtsparc; resources.WriteToFile(ChangeFileExt(paramstr(1),'.o'),writer); resources.Free; writer.Free; end.
Note that the file to convert is taken from the command line. Its format is automatically detected among res (resreader), coff (coffreader), elf (elfreader), PE (winpeimagereader, e.g. a Windows exe or dll), and is written as an ELF file for SPARC. Note that we had to use elfconsts unit since we used emtsparc constant to specify the machine type of the object file to generate.
With a small change to the above program we can let the user know which reader was selected to read the input file: we can use TResources.FindReader class method to obtain the appropriate reader for a given stream.
program res3; {$mode objfpc} uses Classes, SysUtils, resource, resreader, coffreader, elfreader, winpeimagereader, //readers elfwriter, elfconsts; var resources : TResources; writer : TElfResourceWriter; reader : TAbstractResourceReader; inFile : TFileStream; begin resources:=TResources.Create; inFile:=TFileStream.Create(paramstr(1), fmOpenRead or fmShareDenyNone); reader:=TResources.FindReader(inFile); writeln('Selected reader: ',reader.Description); resources.LoadFromStream(inFile,reader); writer:=TElfResourceWriter.Create; writer.MachineType:=emtsparc; resources.WriteToFile(ChangeFileExt(paramstr(1),'.o'),writer); resources.Free; reader.Free; writer.Free; inFile.Free; end.
Output example:
user@localhost:~$ ./res3 myresource.res Selected reader: .res resource reader user@localhost:~$
Single resources
You can do more with resources than simply converting between file formats.
TResources.Items property provides a simple way to access all resources contained in the TResources object.
In the following example we read a resource file and then dump each resource data in a file whose name is built from type and name of the dumped resource.
program res4; {$mode objfpc} uses Classes, SysUtils, resource, resreader; var resources : TResources; dumpFile : TFileStream; i : integer; fname : string; begin resources:=TResources.Create; resources.LoadFromFile('myresource.res'); or:=0 to resources.Count-1 do begin fname:=resources[i].Typee+'_'+resources[i].Name.Name; dumpFile:=TFileStream.Create(fname,fmCreate or fmShareDenyWrite); dumpFile.CopyFrom(resources[i].RawData,resources[i].RawData.Size); dumpFile.Free; end; resources.Free; end.
This code simply copies the content of each resource's RawData stream to a file stream, whose name is resourcetype_resourcename.
Resource raw data isn't always what one expected, however. While some resource types simply contain a copy of a file in their raw data, other types do some processing, so that dumping raw data doesn't result in a file in the format one expected.
E.g. a resource of type RT_MANIFEST is of the former type: its raw data is like an XML manifest file. On the other hand, in a resource of type RT_BITMAP the RawData stream isn't like a BMP file.
For this reason, several classes (descendants of TAbstractResource) are provided to handle the peculiarities of this or that resource type. Much like it's done with readers and writers, resource classes can be registered: adding the unit that contains a resource class to the uses clause of your program registers that class. This way, when resources are read from a file, they are created with the class that is registered for their type (the class responsible to do this is TResourceFactory, but probably you won't need to use it unless you're implementing a new resource reader or resource class).
In the following example, we read a resource file and then dump data of each resource of type RT_BITMAP as a BMP file.
program res5; {$mode objfpc} uses Classes, SysUtils, resource, resreader, bitmapresource; var resources : TResources; dumpFile : TFileStream; i : integer; fname : string; begin resources:=TResources.Create; resources.LoadFromFile('myresource.res'); or:=0 to resources.Count-1 do if resources[i] is TBitmapResource then with resources[i] as TBitmapResource do begin fname:=Name.Name+'.bmp'; dumpFile:=TFileStream.Create(fname,fmCreate or fmShareDenyWrite); dumpFile.CopyFrom(BitmapData,BitmapData.Size); dumpFile.Free; end; resources.Free; end.
Note that we included bitmapresource in the uses clause of our program. This way, resources of type RT_BITMAP are created from TBitmapResource class. This class provides a stream, BitmapData that allows resource raw data to be accessed as if it was a bmp file.
We can of course do the opposite. In the following code we are creating a manifest resource from manifest.xml file.
program res6; {$mode objfpc} uses Classes, SysUtils, resource, reswriter; var resources : TResources; inFile : TFileStream; res : TGenericResource; rname,typeResourceDesc; begin inFile:=TFileStream.Create('manifest.xml',fmOpenRead or fmShareDenyNone); typeesourceDesc.Create(RT_MANIFEST); rname:=TResourceDesc.Create(1); res:=TGenericResource.Create(typeme); typee; //no longer needed rname.Free; res.SetCustomRawDataStream(inFile); resources:=TResources.Create; resources.Add(res); resources.WriteToFile('myresource.res'); resources.Free; //frees res as well inFile.Free; end.
Note that resources of type RT_MANIFEST contain a straight copy of a xml file, so TGenericResource class fits our needs. TGenericResource is a basic implementation of TAbstractResource. It is the default class used by TResourceFactory when it must create a resource whose type wasn't registered with any resource class.
Please note that instead of copying inFile contents to RawData we used SetCustomRawDataStream method: it sets a stream as the underlying stream for RawData, so that when final resource file is written, data is read directly from the original file.
Let's see a similar example, in which we use a specific class instead of TGenericResource. In the following code we are creating a resource containing the main program icon, which is read from mainicon.ico file.
program res7; {$mode objfpc} uses Classes, SysUtils, resource, reswriter, groupiconresource; var resources : TResources; inFile : TFileStream; iconres : TGroupIconResource; name : TResourceDesc; begin inFile:=TFileStream.Create('mainicon.ico',fmOpenRead or fmShareDenyNone); name:=TResourceDesc.Create('MAINICON'); //type is always RT_GROUP_ICON for this resource class iconres:=TGroupIconResource.Create(nil,name); iconres.SetCustomItemDataStream(inFile); resources:=TResources.Create; resources.Add(iconres); resources.WriteToFile('myicon.res'); resources.Free; //frees iconres as well inFile.Free; name.Free; end.
In this program we created a new TGroupIconResource with 'MAINICON' as name, and we loaded its contents from file 'mainicon.ico'. Please note that RT_GROUP_ICON resource raw data doesn't contain a .ico file, so we have to write to ItemData which is a ico-like stream. As we did for res6 program, we tell the resource to use our stream as the underlying stream for resource data: the only difference is that we are using TGroupResource.SetCustomItemDataStream instead of TAbstractResource.SetCustomRawDataStream method, for obvious reasons.
Other resource types
There are other resource types that allow to easily deal with resource data. E.g. TVersionResource makes it easy to create and read version information for Windows executables and dlls, TStringTableResource deals with string tables, and so on.
Data caching
Whenever possible, fcl-res tries to avoid to duplicate data. Generally a reader doesn't copy resource data from the original stream to RawData stream: instead, it only informs the resource about where its raw data is in the original stream. RawData uses a caching system so that it appears as a stream while it only redirects operations to its underlying stream, with a copy-on-write mechanism. Of course this behaviour can be controlled by the user. For further information, see TAbstractResource and TAbstractResource.RawData.